-- DPSCheck.lua

------------------------------------------------------------
-- saved variables -----------------------------------------
------------------------------------------------------------

DPSCheck_CharacterData = {}

local function expose_table(table, sub_table_name)
  if table[sub_table_name] == nil then
    table[sub_table_name] = {}
  end

  return table[sub_table_name]
end

local combat_segments = nil
local current_state = nil

------------------------------------------------------------
-- session variables ---------------------------------------
------------------------------------------------------------

local current_filter = nil

local player_guid = nil

local priority_functions = {}

local current_reports = {}
local current_report_selected = nil
local current_summaries = {}

local report_snapshot = {}

------------------------------------------------------------
-- internal configuration ----------------------------------
------------------------------------------------------------

local REPORT_ROWS = 8

------------------------------------------------------------
-- helper functions ----------------------------------------
------------------------------------------------------------

function DPSCheck_get(t, k, d)
  local output = t[k]
  if output == nil then
    output = d
  end
  return output
end
local get = DPSCheck_get

function DPSCheck_get2(t, k1, k2, d)
  if t[k1] == nil then
    return d
  else
    return get(t[k1], k2, d)
  end
end
local get2 = DPSCheck_get2

function DPSCheck_get3(t, k1, k2, k3, d)
  if t[k1] == nil then
    return d
  else
    return get2(t[k1], k2, k3, d)
  end
end
local get3 = DPSCheck_get3

local function get_key(s)
  return s:lower():gsub(' ', '_')
end

local function shallow_eq(a, b)
  if a == b then
    return true
  end

  if type(a) ~= type(b) then
    return false
  end

  if type(a) ~= 'table' then
    return false
  end

  -- compare elements of a to corresponding elements in b
  for k, v_a in pairs(a) do
    if not shallow_eq(v_a, b[k]) then
      return false
    end
  end

  -- look for elements in b that are not in a
  for k, v_b in pairs(b) do
    if a[k] == nil then
      return false
    end
  end

  return true
end

local function scan_group(trace_temp)
  local group_unit_ids = {'player', 'pet'}
  for prefix, get_num_func in pairs({raid = GetNumRaidMembers, party = GetNumPartyMembers}) do
    for i = 1, get_num_func() do
      table.insert(group_unit_ids, prefix .. i)
      table.insert(group_unit_ids, prefix .. i .. 'pet')
    end
  end

  local combat_temp = {}
  for _, unit_id in ipairs(group_unit_ids) do
    if UnitAffectingCombat(unit_id) then
      combat_temp[UnitName(unit_id)] = true
    end
  end

  trace_temp.group_affecting_combat = {}
  for unit_id, _ in pairs(combat_temp) do
    table.insert(trace_temp.group_affecting_combat, unit_id)
  end
  table.sort(trace_temp.group_affecting_combat)
end

local function scan_guids(event, combat_segment)
  if combat_segment.guid_names == nil then
    combat_segment.guid_names = {}
  end

  if event.source_guid ~= nil then
    combat_segment.guid_names[event.source_guid] = event.source_name
  end
  if event.dest_guid ~= nil then
    combat_segment.guid_names[event.dest_guid] = event.dest_name
  end
end

local function scan_talents(trace_temp)

  trace_temp.talents = {}
  for tab_index = 1, 3 do
    local num_talents = GetNumTalents(tab_index, false, false)

    for talent_index = 1, num_talents do
      local name, icon_texture, tier, column, rank = GetTalentInfo(tab_index, talent_index, false, false, nil)
      trace_temp.talents[name] = rank
    end
  end

  return num_talent_points
end

function DPSCheck_update_sum(table, k, v)
  if table[k] == nil then
    table[k] = v
  else
    table[k] = table[k] + v
  end
end
local update_sum = DPSCheck_update_sum

local function update_sum2(table, k1, k2, v)
  if table[k1] == nil then
    table[k1] = {}
  end
  return update_sum(table[k1], k2, v)
end

local function update_cumulative_statistics(t_cum, t_new)
  for k, v in pairs(t_new) do
    if type(v) == 'table' then
      if t_cum[k] == nil then
        t_cum[k] = {}
      end
      update_cumulative_statistics(t_cum[k], t_new[k])
    else
      update_sum(t_cum, k, v)
    end
  end
end

------------------------------------------------------------
-- combat log parsers --------------------------------------
------------------------------------------------------------

local combat_log_parsers = {}
local combat_log_prefix_parsers = {}
local combat_log_suffix_parsers = {}

local function combat_log_basic (trace_temp, prefix, suffix, ...)
  local prefix_handler = combat_log_prefix_parsers[prefix]
  if prefix_handler == nil then
    print_debug(format('missing combat log prefix handler for %s', prefix))
    return
  end

  local suffix_handler = combat_log_suffix_parsers[suffix]
  if suffix_handler == nil then
    print_debug(format('missing combat log suffix handler for %s', suffix))
    return
  end

  prefix_handler(trace_temp, suffix_handler, ...)
  return trace_temp
end

-- prefix parsers

combat_log_prefix_parsers['ENVIRONMENTAL'] = function (trace_temp, suffix_handler, environmental_type, ...)
  trace_temp.environmental_type = environmental_type

  suffix_handler(...)
end

combat_log_prefix_parsers['RANGE'] = function (trace_temp, suffix_handler, spell_id, spell_name, spell_school, ...)
  trace_temp.spell_id = spell_id
  trace_temp.spell_name = spell_name
  trace_temp.spell_school = spell_school

  suffix_handler(trace_temp, ...)
end

combat_log_prefix_parsers['SPELL'] = combat_log_prefix_parsers['RANGE']
combat_log_prefix_parsers['SPELL_BUILDING'] = combat_log_prefix_parsers['SPELL']
combat_log_prefix_parsers['SPELL_PERIODIC'] = combat_log_prefix_parsers['SPELL']

combat_log_prefix_parsers['SWING'] = function (trace_temp, suffix_handler, ...)
  suffix_handler(trace_temp)
end

-- suffix parsers

combat_log_suffix_parsers['AURA_APPLIED'] = function (trace_temp, aura_type)
  trace_temp.aura_type = aura_type
end

combat_log_suffix_parsers['AURA_REFRESH'] = function (trace_temp, aura_type)
  trace_temp.aura_type = aura_type
end

combat_log_suffix_parsers['AURA_REMOVED'] = function (trace_temp, aura_type)
  trace_temp.aura_type = aura_type
end

combat_log_suffix_parsers['CAST_SUCCESS'] = function (trace_temp)
end

combat_log_suffix_parsers['CAST_START'] = function (trace_temp)
end

combat_log_suffix_parsers['DAMAGE'] = function (trace_temp, damage_amount, overkill, damage_school, damage_resisted, damage_blocked, damage_absorbed, critical, glancing, crushing)
  trace_temp.damage_amount = damage_amount
  -- these fields are not used yet
  --trace_temp.overkill = overkill
  --trace_temp.damage_school = damage_school
  --trace_temp.damage_resisted = damage_resisted
  --trace_temp.damage_blocked = damage_blocked
  --trace_temp.damage_absorbed = damage_absorbed
  --trace_temp.critical = critical
  --trace_temp.glancing = glancing
  --trace_temp.crushing = crushing
end

combat_log_suffix_parsers['ENERGIZE'] = function (trace_temp, energize_amount, power_type)
  trace_temp.energize_amount = energize_amount
  trace_temp.power_type = power_type
end

combat_log_suffix_parsers['MISSED'] = function (trace_temp, miss_type, missed_amount)
  trace_temp.miss_type = miss_type
  trace_temp.missed_amount = missed_amount
end

-- log parsers

combat_log_parsers['RANGE_DAMAGE'] = function(trace_temp, ...)
  return combat_log_basic(trace_temp, 'RANGE', 'DAMAGE', ...)
end

combat_log_parsers['RANGE_MISSED'] = function(trace_temp, ...)
  return combat_log_basic(trace_temp, 'RANGE', 'MISSED', ...)
end

combat_log_parsers['SPELL_AURA_APPLIED'] = function(trace_temp, ...)
  return combat_log_basic(trace_temp, 'SPELL', 'AURA_APPLIED', ...)
end

combat_log_parsers['SPELL_AURA_REFRESH'] = function(trace_temp, ...)
  return combat_log_basic(trace_temp, 'SPELL', 'AURA_REFRESH', ...)
end

--combat_log_parsers['SPELL_AURA_REMOVED'] = function(trace_temp, ...)
--  return combat_log_basic(trace_temp, 'SPELL', 'AURA_REMOVED', ...)
--end

--combat_log_parsers['SPELL_CAST_FAILED'] = function(trace_temp, ...)
--  return combat_log_basic(trace_temp, 'SPELL', 'CAST_FAILED', ...)
--end

--combat_log_parsers['SPELL_CAST_START'] = function (trace_temp, ...)
--  return combat_log_basic(trace_temp, 'SPELL', 'CAST_START', ...)
--end

combat_log_parsers['SPELL_CAST_SUCCESS'] = function(trace_temp, ...)
  return combat_log_basic(trace_temp, 'SPELL', 'CAST_SUCCESS', ...)
end

combat_log_parsers['SPELL_DAMAGE'] = function(trace_temp, ...)
  return combat_log_basic(trace_temp, 'SPELL', 'DAMAGE', ...)
end

--combat_log_parsers['SPELL_ENERGIZE'] = function(trace_temp, ...)
--  return combat_log_basic(trace_temp, 'SPELL', 'ENERGIZE', ...)
--end

combat_log_parsers['SPELL_PERIODIC_DAMAGE'] = function(trace_temp, ...)
  return combat_log_basic(trace_temp, 'SPELL_PERIODIC', 'DAMAGE', ...)
end

--combat_log_parsers['SPELL_PERIODIC_ENERGIZE'] = function(trace_temp, ...)
--  return combat_log_basic(trace_temp, 'SPELL_PERIODIC', 'ENERGIZE', ...)
--end

--combat_log_parsers['SPELL_PERIODIC_HEAL'] = function(trace_temp, ...)
--  return combat_log_basic(trace_temp, 'SPELL_PERIODIC', 'HEAL', ...)
--end

combat_log_parsers['SWING_DAMAGE'] = function(trace_temp, ...)
  return combat_log_basic(trace_temp, 'SWING', 'DAMAGE', ...)
end

combat_log_parsers['SWING_MISSED'] = function(trace_temp, ...)
  return combat_log_basic(trace_temp, 'SWING', 'MISSED', ...)
end

--combat_log_parsers['UNIT_DIED'] = function(trace_temp)
--  return trace_temp
--end

------------------------------------------------------------
-- event handlers ------------------------------------------
------------------------------------------------------------

local event_handlers = {}

event_handlers['ACTIVE_TALENT_GROUP_CHANGED'] = function(trace_temp)
  scan_talents(trace_temp)
  return trace_temp
end

event_handlers['COMBAT_LOG_EVENT_UNFILTERED'] = function(trace_temp, timestamp, subevent, source_guid, source_name, source_flags, dest_guid, dest_name, dest_flags, ...)
  -- skip if missing subevent
  if combat_log_parsers[subevent] == nil then
    return
  end

  -- change this to check source flags for pet too
  local keep_event = bit.band(bit.bor(source_flags, dest_flags), COMBATLOG_OBJECT_AFFILIATION_MINE) > 0
  if not keep_event then
    return
  end

  -- basic trace data
  trace_temp.timestamp = timestamp
  trace_temp.subevent = subevent
  trace_temp.source_guid = source_guid
  trace_temp.source_name = source_name
  trace_temp.source_flags = source_flags
  trace_temp.dest_guid = dest_guid
  trace_temp.dest_name = dest_name
  trace_temp.dest_flags = dest_flags
  
  -- specialized handler
  return combat_log_parsers[subevent](trace_temp, ...)
end

event_handlers['PLAYER_ALIVE'] = function(trace_temp)
  scan_talents(trace_temp)
  return trace_temp
end

event_handlers['PLAYER_ENTERING_WORLD'] = function(trace_temp)
  scan_group(trace_temp)
  return trace_temp
end

event_handlers['PLAYER_LEAVING_WORLD'] = function(trace_temp)
  return trace_temp
end

event_handlers['PLAYER_LOGIN'] = function(trace_temp)
  player_guid = UnitGUID('player')

  trace_temp.player_guid = player_guid
  return trace_temp
end

event_handlers['PLAYER_LOGOUT'] = function(trace_temp)
  return trace_temp
end

event_handlers['PLAYER_REGEN_DISABLED'] = function(trace_temp)
  scan_group(trace_temp)
  return trace_temp
end

event_handlers['PLAYER_REGEN_ENABLED'] = function(trace_temp)
  scan_group(trace_temp)
  return trace_temp
end

event_handlers['PLAYER_TALENT_UPDATE'] = function(trace_temp)
  scan_talents(trace_temp)
  return trace_temp
end

--event_handlers['UNIT_AURA'] = function(trace_temp)
--  return trace_temp
--end

event_handlers['UNIT_SPELLCAST_FAILED_QUIET'] = function(trace_temp, unit_id, spell_name, spell_rank)
  if unit_id ~= 'player' then
    return
  end

  trace_temp.unit_id = unit_id
  trace_temp.spell_name = spell_name
  trace_temp.spell_rank = spell_rank
  return trace_temp
end

event_handlers['UNIT_SPELLCAST_SENT'] = function(trace_temp, unit_id, spell_name, spell_rank)
  if unit_id ~= 'player' then
    return
  end

  trace_temp.unit_id = unit_id
  trace_temp.spell_name = spell_name
  trace_temp.spell_rank = spell_rank
  return trace_temp
end

event_handlers['UNIT_SPELLCAST_START'] = function(trace_temp, unit_id, spell_name, spell_rank)
  if unit_id ~= 'player' then
    return
  end

  trace_temp.unit_id = unit_id
  trace_temp.spell_name = spell_name
  trace_temp.spell_rank = spell_rank
  return trace_temp
end

event_handlers['UNIT_SPELLCAST_SUCCEEDED'] = function(trace_temp, unit_id, spell_name, spell_rank)
  if unit_id ~= 'player' then
    return
  end

  trace_temp.unit_id = unit_id
  trace_temp.spell_name = spell_name
  trace_temp.spell_rank = spell_rank
  return trace_temp
end

--event_handlers['UNIT_TARGET'] = function(trace_temp)
--  return trace_temp
--end

event_handlers['VARIABLES_LOADED'] = function(trace_temp)
  combat_segments = expose_table(DPSCheck_CharacterData, 'combat_segments')
  current_state = expose_table(DPSCheck_CharacterData, 'current_state')

  dps_trace = expose_table(DPSCheck_CharacterData, 'dps_trace')
end

------------------------------------------------------------
-- event matching ------------------------------------------
------------------------------------------------------------

local event_patterns = {}
function DPSCheck_push_event_pattern(func, value_filters, key_filters) 
  if key_filters == nil then
    key_filters = {}
  end
  table.insert(event_patterns, {func = func, value_filters = value_filters, key_filters = key_filters})
end
local push_event_pattern = DPSCheck_push_event_pattern

local function match_event_pattern(input_event, event_pattern)
  for k, v in pairs(event_pattern.value_filters) do
    if input_event[k] ~= v then
      return
    end
  end

  for _, k in ipairs(event_pattern.key_filters) do
    if input_event[k] == nil then
      return
    end
  end

  event_pattern.func(current_state, combat_segments[#combat_segments], input_event)
end

local function match_event(input_event)
  if combat_segments == nil then
    return
  end

  for _, event_pattern in ipairs(event_patterns) do
    match_event_pattern(input_event, event_pattern)
  end
end

-- patterns affecting combat segment
push_event_pattern(function (current_state, combat_segment, input_event)
                     current_state.regen_active = false
                   end,
                   {event = 'PLAYER_REGEN_DISABLED'})
push_event_pattern(function (current_state, combat_segment, input_event)
                     current_state.regen_active = true
                   end,
                   {event = 'PLAYER_REGEN_ENABLED'})
push_event_pattern(function (current_state, combat_segment, input_event)
                     if current_state.regen_active then
                       scan_group(input_event)
                     end
                   end,
                   {event = 'COMBAT_LOG_EVENT_UNFILTERED'})
push_event_pattern(function (current_state, combat_segment, input_event)
                     current_state.group_affecting_combat = #(input_event.group_affecting_combat) > 0
                   end,
                   {},
                   {'group_affecting_combat'})
push_event_pattern(function (current_state, combat_segment, input_event)
                     current_state.combat_active = current_state.group_affecting_combat or (not current_state.regen_active) or false
                     -- start a new segment if there is no segment, combat state changed, or we are logging in                      
                     if (combat_segment == nil) or (current_state.combat_active ~= combat_segment.combat_active) or (input_event.event == 'PLAYER_LOGIN') then
                       table.insert(combat_segments, {combat_active = current_state.combat_active, event_counts = {}, statistics = {priorities = {}}})
                     end
                   end,
                   {}, {}) -- always match and update combat status

push_event_pattern(function (current_state, combat_segment, input_event)
                     update_sum(combat_segment.event_counts, input_event.event, 1)
                   end,
                   {},
                   {'event'})

push_event_pattern(function (current_state, combat_segment, input_event)
                     if combat_segment.time_min == nil then
                       combat_segment.time_min = input_event.timestamp
                     end

                     combat_segment.time_max = input_event.timestamp

                     if current_state.combat_active then
                       combat_segment.statistics.active_time = combat_segment.time_max - combat_segment.time_min
                     end
                   end,
                   {},
                   {'timestamp'})

push_event_pattern(function (current_state, combat_segment, input_event)
                     scan_guids(input_event, combat_segment)

                     local other_guid
                     if bit.band(input_event.source_flags, COMBATLOG_OBJECT_AFFILIATION_MINE) > 0 then
                       other_guid = input_event.dest_guid
                     else
                       -- only return damage our side did. otherwise
		       -- weird fights like heroic festergut and
		       -- rotface get confused with external bosses
		       -- like putricide.

                       return
                     end

                     if combat_segment.damage == nil then
                       combat_segment.damage = {}
                     end
                     update_sum(combat_segment.damage, other_guid, input_event.damage_amount)
                   end,
                   {event = 'COMBAT_LOG_EVENT_UNFILTERED'},
                   {'damage_amount'})

push_event_pattern(function (current_state, combat_segment, input_event)
                     update_sum2(combat_segment.statistics, 'spell_aura_applied', input_event.spell_name, 1)
                   end,
                   {event = 'COMBAT_LOG_EVENT_UNFILTERED', subevent = 'SPELL_AURA_APPLIED', source_flags = 1297})           

push_event_pattern(function (current_state, combat_segment, input_event)
                     update_sum2(combat_segment.statistics, 'spell_cast_success', input_event.spell_name, 1)
                   end,
                   {event = 'COMBAT_LOG_EVENT_UNFILTERED', subevent = 'SPELL_CAST_SUCCESS', source_flags = 1297})           

push_event_pattern(function (current_state, combat_segment, input_event)
                     update_sum2(combat_segment.statistics, 'spell_damage', input_event.spell_name, 1)
                   end,
                   {event = 'COMBAT_LOG_EVENT_UNFILTERED', subevent = 'SPELL_DAMAGE', source_flags = 1297})           

push_event_pattern(function (current_state, combat_segment, input_event)
                     if priority_functions[input_event.spell_name] == nil then
                       return
                     end

                     if combat_segment.statistics.priorities[input_event.spell_name] == nil then
                       combat_segment.statistics.priorities[input_event.spell_name] = {count = 0, mistakes = {}}
                     end

                     local spell_stats = combat_segment.statistics.priorities[input_event.spell_name]
		     update_sum(spell_stats, 'count', 1)

                     local spell_names = {}
                     for tab_index = 1,GetNumSpellTabs() do
                       local tab_name, tab_texture, tab_offset, tab_numSpells = GetSpellTabInfo(tab_index)
                       for spell_book_index = tab_offset+1,tab_offset+tab_numSpells do
                         local spell_name = GetSpellInfo(spell_book_index, 'spell')
                         -- spell name will be nill for Call Stabled Pet (now 5 spells)
                         if spell_name ~= nil then
                           spell_names[spell_name] = true
                         end
                       end
                     end

                     local available_spell_names = {}
                     for spell_name, _ in pairs(spell_names) do
                       local start, duration = GetSpellCooldown(spell_name)
                       -- TODO: this is not the right gcd for all classes
                       local off_cooldown = false
                       if start == nil then
                         -- don't bother checking skills unaffected by any cooldown.
                         -- Cold Weather Flying is prime example.
                         off_cooldown = false
                       elseif duration < 1 then
                         off_cooldown = true
                       end

                       if off_cooldown then
                         -- check if other conditions apply, e.g. for Kill Shot
                         local is_usable, not_enough_mana = IsUsableSpell(spell_name)
                         if is_usable then
                           available_spell_names[spell_name] = 1
                         end
                       end
                     end

                     local preferred_spell_name = priority_functions[input_event.spell_name](input_event.spell_name, available_spell_names)
                     if preferred_spell_name ~= nil then
                       update_sum(spell_stats.mistakes, preferred_spell_name, 1)
                     end                     
                   end,
                   {event = 'UNIT_SPELLCAST_SENT', unit_id = 'player'})

push_event_pattern(function (current_state, combat_segment, input_event)
                     current_state.talents = input_event.talents
                   end,
                   {}, 
                   {'talents'})

------------------------------------------------------------
-- setup event handlers ------------------------------------
------------------------------------------------------------

-- create dummy frame to receive events

local event_frame = CreateFrame("Frame", "DPSCheckEventFrame")
event_frame:SetScript("OnEvent", 
                      function (self, event, ...)
                        if type(event_handlers[event]) == 'function' then
                          parsed_event = event_handlers[event]({event = event}, ...)
                          if parsed_event ~= nil then
                            match_event(parsed_event)
                          end
                        end
                      end)

-- register the configured events

for k, v in pairs(event_handlers) do
  event_frame:RegisterEvent(k)
end

------------------------------------------------------------
-- output --------------------------------------------------
------------------------------------------------------------

local function print_chat(m)
  DEFAULT_CHAT_FRAME:AddMessage('DPSCheck: ' .. m)
end

local function print_debug(m)
  if addon_debug then
    DEFAULT_CHAT_FRAME:AddMessage('DPSCheck: ' .. m)
  end
end

------------------------------------------------------------
-- reporting functions -------------------------------------
------------------------------------------------------------

local report_functions = {}

function DPSCheck_push_report_function(report_name, report_category, report_description, report_func)
  table.insert(report_functions, {name = report_name, category = report_category, description = report_description, func = report_func})
end

local function priority_report_function(spell_name, combat_segment)
  if get3(combat_segment, 'statistics', 'priorities', spell_name) == nil then
    return
  end

  local spell_stats = combat_segment.statistics.priorities[spell_name]
  local total_count = spell_stats.count
  local mistake_count = 0
  local mistakes_list = {}
  for k, v in pairs(spell_stats.mistakes) do
    mistake_count = mistake_count + v
    table.insert(mistakes_list, {k, v})
  end
  
  local comment
  if #mistakes_list == 0 then
    comment = ''
  elseif #mistakes_list == 1 then
    comment = format('Preferred: %d %s', mistakes_list[1][2], mistakes_list[1][1])
  else
    table.sort(mistakes_list, function(a,b) return a[2] > b[2] end)
    comment = format('Preferred: %d %s, %d %s', mistakes_list[1][2], mistakes_list[1][1], mistakes_list[2][2], mistakes_list[2][1])
    if #mistakes_list > 2 then
      comment = comment .. ', ...'
    end
  end

  return {total_count - mistake_count, total_count, comment}
end
function DPSCheck_push_priority_function(spell_name, priority_description, priority_func)
  local function temp_func (combat_segment)
    return priority_report_function(spell_name, combat_segment)
  end

  priority_functions[spell_name] = priority_func
  table.insert(report_functions, {name = spell_name:lower() .. ' usage', category = 'Priority Checks', description = priority_description, func = temp_func})
end

local function run_reports_helper(combat_segment, output)
  local temp_results = {}

  for _, t in ipairs(report_functions) do
    temp_results[t.name] = t.func(combat_segment)
  end

  if next(temp_results) ~= nil then
    table.insert(output, {main_target_name = main_target_name, time = combat_segment.time_min, results = temp_results})
  end
end

local function run_reports()
  local output = {}

  local recent_fights = {}
  local damage_fights = {}

  combined_segment = {}
  combined_segment.time_min = 'overall'
  combined_segment.statistics = {}

  for i, combat_segment in ipairs(combat_segments) do
    local main_target_guid
    local main_target_damage
    if combat_segment.damage ~= nil then
      for other_guid, other_damage in pairs(combat_segment.damage) do
        if main_target_damage == nil then
          main_target_guid = other_guid
          main_target_damage = other_damage
        elseif main_target_damage < other_damage then
          main_target_guid = other_guid
          main_target_damage = other_damage
        end
      end
    end

    local main_target_name = nil
    if combat_segment.guid_names ~= nil then
      main_target_name = combat_segment.guid_names[main_target_guid]
    end

    if main_target_name ~= nil then
      recent_fights[main_target_name] = combat_segment.time_min

      if damage_fights[main_target_name] == nil then
        damage_fights[main_target_name] = main_target_damage
      elseif damage_fights[main_target_name] < main_target_damage then
        damage_fights[main_target_name] = main_target_damage
      end
    end

    local main_target_match = false
    if main_target_name ~= nil then
      if current_filter == nil then
        main_target_match = true
      elseif main_target_name:lower():find(current_filter) ~= nil then
        main_target_match = true
      end
    end

    local show_segment = main_target_match
    if show_segment then
      run_reports_helper(combat_segment, output)
      update_cumulative_statistics(combined_segment.statistics, combat_segment.statistics)
    end
  end

  local combined_output = {}
  run_reports_helper(combined_segment, combined_output)
  if #combined_output > 0 then
    table.insert(output, combined_output[1])
    current_summaries = combined_output[1].results
  else
    current_summaries = {}
  end

  most_damage_fights = {}
  most_recent_fights = {}
  for n in pairs(recent_fights) do
    table.insert(most_damage_fights, n)
    table.insert(most_recent_fights, n)
  end
  table.sort(most_damage_fights, function (a, b) return damage_fights[a] > damage_fights[b] end)
  table.sort(most_recent_fights, function (a, b) return recent_fights[a] > recent_fights[b] end)

  return output
end

local function select_report()
  if current_reports[current_report_selected] ~= nil then
    return
  end

  local dropdown = DPSCheck_ReportFrameTypeDropDown

  for i, t in ipairs(report_functions) do
    if current_reports[t.name] ~= nil then
      current_report_selected = t.name
      UIDropDownMenu_SetSelectedValue(dropdown, i, false)
      UIDropDownMenu_SetText(dropdown, t.name)
      return
    end
  end

  current_selected_report = nil
end

local function show_filtered_report_handler(self, filter)
  current_filter = filter:lower()
  DPSCheck_Update()
  DPSCheck_DataFrame_Update(DPSCheck_DataFrame)
  DPSCheck_ReportFrame:Show()
end

local function get_summary_info()
  local output = {}

  -- don't use UIDropDownMenu_CreateInfo() because it recycles table

  local function add_header(t)
    local info = {}
    info.text = t
    info.isTitle = 1
    table.insert(output, info)
  end
  local function add_report(i, n)
    local info = {}
    info.text = n
    info.value = i
    if current_summaries[n] == nil then
      info.disabled = true
    else
      info.numerator = math.floor(current_summaries[n][1] * 10) / 10
      info.denominator = math.floor(current_summaries[n][2] * 10) / 10

      info.text_left = n
      info.text = info.text_left .. ' (' .. info.numerator .. '/' .. info.denominator .. ')'
      info.func = DPSCheck_ReportFrameTypeDropDown_Check
      info.arg1 = n
      info.arg2 = i
    end
    table.insert(output, info)
  end   

  last_category = nil
  for i, t in ipairs(report_functions) do
    if t.category ~= last_category then
      add_header(t.category)
      last_category = t.category
    end

    add_report(i, t.name)
  end

  if most_recent_fights ~= nil then
    add_header('Recent Targets')
    for i, n in ipairs(most_recent_fights) do
      if i <= 5 then
        local info = {}
        info.text = n
        info.func = show_filtered_report_handler
        info.arg1 = n
        table.insert(output, info)
      end
    end
  end

  if most_damage_fights ~= nil then
    add_header('Highest Damage Targets')
    for i, n in ipairs(most_damage_fights) do
      if i <= 10 then
        local info = {}
        info.text = n
        info.func = show_filtered_report_handler
        info.arg1 = n
        table.insert(output, info)
      end
    end
  end

  return output
end

------------------------------------------------------------
-- report display ------------------------------------------
------------------------------------------------------------

function DPSCheck_ReportFrameTypeDropDown_Check(self, arg1, arg2, checked)
  local dropdown = getglobal('DPSCheck_ReportFrameTypeDropDown')
  current_report_selected = arg1
  UIDropDownMenu_SetSelectedValue(dropdown, arg2, false)
  DPSCheck_Update_Rows()
  DPSCheck_DataFrame_Update(DPSCheck_DataFrame)
end

function DPSCheck_ReportFrameTypeDropDown_Initialize(level)
  for i, s in ipairs(get_summary_info()) do
    UIDropDownMenu_AddButton(s)
  end
end

function DPSCheck_Update()
  local results = run_reports()
  
  local results_by_name = {}
  for _, r in ipairs(results) do
    for name, result in pairs(r.results) do
      if results_by_name[name] == nil then
        results_by_name[name] = {}
      end
      table.insert(results_by_name[name], {time = r.time, result = result})
    end
  end
  current_reports = results_by_name -- TODO: clean this up
  select_report()

  DPSCheck_Update_Rows()
end

function DPSCheck_Update_Rows()
  DPSCheck_ReportFrameTitleText:SetText("DPSCheck Report")

  report_snapshot = {}
  --if args == nil then
  --  table.insert(report_snapshot, 'reporting on all fights')
  --else
  --  -- TODO: if only one matching name, show that name here
  --  table.insert(report_snapshot, 'reporting on ' .. args .. ' fights')
  --end

  local dropdown = DPSCheck_ReportFrameTypeDropDown
  local description = nil
  if dropdown.selectedValue ~= nil then
    description =  report_functions[dropdown.selectedValue].description
  end
  if description == nil then
    description = ''
  end
  DPSCheck_ReportFrameDescriptionText:SetText(description)

  local current_results = current_reports[current_report_selected]
  if current_results ~= nil then
    for _, t2 in ipairs(current_results) do
      local timestamp
      if type(t2.time) == 'string' then
        timestamp = t2.time
      elseif t2.time ~= nil then
        timestamp = date('%m-%d %H:%M', t2.time)
      else
        timestamp = '~~~~~~~~'
      end
      table.insert(report_snapshot, {timestamp, t2.result[1], t2.result[2], t2.result[3]})
    end
  end
end

function DPSCheck_DataFrame_Update(self)
  local scroll_frame = DPSCheck_DataFrameScrollFrame
  local item_offset = FauxScrollFrame_GetOffset(scroll_frame)

  for i=1,REPORT_ROWS do
    local row_name = DPSCheck_DataFrame:GetName() .. 'Row' .. i
    local item_index = i + item_offset
    if item_index <= #report_snapshot then
      local report_row = report_snapshot[item_index]
      getglobal(row_name .. 'BarFightName'):SetText(report_row[1])
      getglobal(row_name .. 'Bar'):SetMinMaxValues(0, report_row[3])
      getglobal(row_name .. 'Bar'):SetValue(report_row[2])

      local numerator = math.floor(report_row[2] * 10) / 10
      local denominator = math.floor(report_row[3] * 10) / 10

      getglobal(row_name .. 'BarScore'):SetText(numerator .. '/' .. denominator)
      getglobal(row_name .. 'BarComment'):SetText(report_row[4])
      getglobal(row_name):Show()
    else
      getglobal(row_name):Hide()
    end    
  end

  FauxScrollFrame_Update(scroll_frame, #report_snapshot, REPORT_ROWS, 35);
end

------------------------------------------------------------
-- libdatabroker integration -------------------------------
------------------------------------------------------------

local ldb = LibStub:GetLibrary("LibDataBroker-1.1")

ldb_config = {}
ldb_config['type'] = 'data source'
ldb_config['icon'] = 'Interface\\Icons\\Ability_Whirlwind'

local ldb_icon = ldb and LibStub:GetLibrary("LibDBIcon-1.0")

local ldb_icon_config = {}
ldb_icon_config['hide'] = false
ldb_icon_config['minimapPos'] = 245
ldb_icon_config['radius'] = 80

function ldb_icon_config:Toggle()
  ldb_icon_config['hide'] = not ldb_icon_config['hide']
  if ldb_icon_config['hide'] then
    ldb_icon:Hide('DPSCheck')
  else 
    ldb_icon:Show('DPSCheck')
  end
end

function ldb_config:OnClick(button) 
  if button == 'LeftButton' then
    if DPSCheck_ReportFrame:IsVisible() then
      DPSCheck_ReportFrame:Hide()
    else
      DPSCheck_Update()
      DPSCheck_DataFrame_Update(DPSCheck_DataFrame)
      DPSCheck_ReportFrame:Show()
    end
  elseif button == 'RightButton' then
    ldb_icon_config:Toggle()
  end
end

function ldb_config:OnTooltipShow()
  DPSCheck_Update()

  self:AddLine('|cff0090ffDPSCheck|r')
  self:AddLine("Left-click to toggle full report")
  self:AddLine("Right-click to toggle minimap icon")

  if current_filter ~= nil then
    self:AddLine(' ')
    self:AddLine(format('current filter = %s', current_filter))
  end
  
  for _, s in ipairs(get_summary_info()) do
    if s.isTitle then
      self:AddLine(' ')
      self:AddLine(format('|cffffffff%s|r', s.text))
    elseif s.denominator ~= nil then
      local score = s.numerator / s.denominator
      local r = 0.0
      local g = 0.0
      local b = 0.0
      if score >= 0.9 then
        g = 1.0
      elseif score >= 0.8 then
        r = 1.0
        g = 1.0
      else
        r = 1.0
      end

      self:AddDoubleLine(s.text_left, '' .. s.numerator .. '/' .. s.denominator, nil, nil, nil, r, g, b)
    else
      self:AddLine(format('|cff808080%s', s.text))
    end
  end
end

function ldb_config:OnEnter()
	 GameTooltip:SetOwner(self, "ANCHOR_NONE")
	 GameTooltip:SetPoint("TOPLEFT", self, "BOTTOMLEFT")
	 GameTooltip:ClearLines()
	 ldb_config.OnTooltipShow(GameTooltip)
	 GameTooltip:Show()
end
function ldb_config:OnLeave()
	 GameTooltip:Hide()
end

local ldb_launcher = ldb:NewDataObject("DPSCheck", ldb_config)
ldb_icon:Register("DPSCheck", ldb_launcher, ldb_icon_config)

------------------------------------------------------------
-- slash commands ------------------------------------------
------------------------------------------------------------

local function dps_usage()
  print_chat('DPS usage:')
  print_chat('  /dps help                this help text')
  print_chat('')
  print_chat('  /dps check [filter]     report on fights matching filter')
--  print_chat('  /dps on             starts recording data')
--  print_chat('  /dps off            stops recording data')
  print_chat('  /dps clear          clears recorded data')
  print_chat('  /dps debug          enables debugging interface')
  print_chat('  /dps minimap        toggle minimap icon')
end

-- generic dispatch function for parsing slash commands

local function dps_dispatch(dispatch_table, command)
  -- various details from World of Warcraft Programming, page 261-262

  local subcommand, args = command:match("([^ ]+) (.+)")
  if subcommand == nil then
    subcommand = command
    args = nil
  end

  if dispatch_table[subcommand] == nil then
    dps_usage()
    return
  end

  dispatch_table[subcommand](args)
end

local dps_commands = {}

dps_commands['help'] = dps_usage

dps_commands['check'] = function(args)
  current_filter = args

  DPSCheck_Update()
  DPSCheck_DataFrame_Update(DPSCheck_DataFrame)
  DPSCheck_ReportFrame:Show()
end

dps_commands['clear'] = function(args)
  DPSCheck_CharacterData.combat_segments = nil
  combat_segments = expose_table(DPSCheck_CharacterData, 'combat_segments')

  current_filter = nil

  DPSCheck_Update()
  DPSCheck_DataFrame_Update(DPSCheck_DataFrame)
end

local debug_active = nil
dps_commands['debug'] = function(args)
  debug_active = not debug_active
  if debug_active then
    DPSCheck_ReportFrameRefreshButton:Show()
  else
    DPSCheck_ReportFrameRefreshButton:Hide()
  end
end

dps_commands['minimap'] = function(args)
  ldb_icon_config:Toggle()
end

SLASH_DPS1 = '/dps'
SlashCmdList['DPS'] = function(command)
  dps_dispatch(dps_commands, command)
end

------------------------------------------------------------
-- text welcome --------------------------------------------
------------------------------------------------------------

print_chat('Type "/dps help" for help or "/dps check" to dive in.')

